<?php
/**
 * @author( author = 'xbruce' )
 * @datetime( datetime = '2017年7月2日 下午3:29:44' )
 * @comment( comment='ip地址工具' )
 */
class IpUtils
{
    private static $_instance = null;

    private $_nredis = null;
    
    private $arrIpsTmp = array();
    
    const REDIS_SS_IPS = 'l_ss_ips';
    
    const GREP_IP = "/[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}/";
    
    private function __construct( $redis = null )
    {
        if( $redis )
        {
            $this->_nredis = $redis;
        }

        assert( $redis, 'redis实例不能为空！' );
    }

    static public function getInstance( $redis = null )
    {
        assert( $redis, 'redis实例不能为空！' );
        
        if( !$redis )
        {
            echo 'redis实例不能为空！', PHP_EOL;
            return null;
        }
        
        if( is_null( self::$_instance ) )
        {
            self::$_instance = new IpUtils( $redis );
        }
        
        return self::$_instance;
    }
  
    /**
     * 
     * @author( author='xbruce' )
     * @date( date = '2017年7月2日' )
     * @comment( comment = '读文件到内存中以方便写查询缓存' )
     * @method( method = '' )
     * @op( op = '' )
     * @param string $strFileName
     * @return boolean
     */
    public function initIpLibFromFile( string $strFileName = null )
    {
        set_time_limit( 0 );
        ini_set( 'memory_limit', '1024M' );
        
        if( !file_exists( $strFileName ))
        {
            return false;
        }
        
//         $this->removeSetKeys();
        
        $fIp = fopen( $strFileName, 'r' );
        
        if( !$fIp )
        {
            return false;
        }
                
        echo '正在进行缓存请稍后...' . PHP_EOL;
        $ip = array();
        
        $iIndex = 0;
        while( ($line = trim( fgets( $fIp ))) != '' )
        {
            $line = mb_convert_encoding( $line, 'UTF8', 'GBK' );
            
            preg_match_all( "/[^\s]+/", $line, $arrMatches );
            
            if( count( $arrMatches[0] ) < 3 )
            {
                return false;
            }
            
            $arrIpInfo = $arrMatches[0];
            $ip['ip_begin'] = $this->ip2longCust( $arrIpInfo[0] );
            $ip['ip_end'] = $this->ip2longCust( $arrIpInfo[1] );
            
            $ip['info'] = '';
            $key = $arrIpInfo[2];
            $iInfoCnt = count( $arrIpInfo );
            for( $i = 2; $i < $iInfoCnt; ++$i )
            {
                $ip['info'] .= ' ' . $arrIpInfo[$i];
            }
            
            $strKey = md5( $key );
            
            $this->arrIpsTmp[ $strKey ][$ip['ip_begin']] = $ip;

            unset($ip);
        }
        
        fclose( $fIp );
        
        echo '写缓存结束！' . PHP_EOL;
        
        return true;
    }
    
    /**
     * 
     * @author( author='xbruce' )
     * @date( date = '2017年7月2日' )
     * @comment( comment = '' )
     * @method( method = '' )
     * @op( op = '' )
     */
    public function combineAdjacentIp()
    {
        ini_set( 'memory_limit', '1024M' );
        
        $arrDupIps = array();
        $arrKeys = $this->arrIpsTmp;
        
        $iIndex = 0;
        
        foreach( $arrKeys as $v )
        {     
            $arrIps = $v;
            foreach( $arrIps as $ip )
            {
                $arrDupIps[ $ip['ip_begin'] ] = $ip;
            }
            
            unset( $arrIps );
                        
            if( uksort( $arrDupIps, function( $left, $right ){
                if( $left < $right )
                {
                    return -1;
                }
                else if( $left == $right )
                {
                    return 0;
                }
                else 
                {
                    return 1;
                }
            }))
            {
                $iCnt = 0;
                
                $arrTemp = array();
                foreach( $arrDupIps as $key => $val )
                {
                    $arrTemp[$iCnt++] = $val;
                }
                unset( $arrDupIps );
                
                $iTargetIndex = 0;
                
                $curIp = $arrTemp[0];
                
                if( $iCnt > 1 )
                {
                    for( $i = 0; $i < $iCnt; ++$i )
                    {
                        if( $i + 1 < $iCnt && ( $curIp['ip_end'] + 1 == $arrTemp[ $i + 1 ]['ip_begin'] ))
                        {
                            $curIp['ip_end'] = $arrTemp[ $i + 1 ]['ip_end'];
                        }
                        else 
                        {
                            ++$iIndex;
                            $this->_nredis->zAdd( self::REDIS_SS_IPS, $curIp['ip_begin'], json_encode( $curIp ));
                            
                            if( $i + 1 < $iCnt )
                            {
                                $curIp = $arrTemp[$i+1];
                            }
                        }
                        
                    }
                }
                else if( 1 == $iCnt )
                {
                    $this->_nredis->zAdd( self::REDIS_SS_IPS, $curIp['ip_begin'], json_encode( $curIp ));
                    ++$iIndex;                    
                }

                unset( $arrTargets );
                unset( $arrTemp );
                
                echo '已处理到第', $iIndex, '个', PHP_EOL;
            }
            else
            {
                echo '排序失败，请检查错误原因！', PHP_EOL;
                exit();
            }
            
        }
        
        $this->_nredis->bgSave();
        echo PHP_EOL, '成功精简IP地址库！', PHP_EOL;
    }
    
    
    /**
     * 
     * @author( author='xbruce' )
     * @date( date = '2017年7月2日' )
     * @comment( comment = '取IP地址相关信息' )
     * @method( method = '' )
     * @op( op = '' )
     * @param string $strIp
     */
    public function getIpLocation( string $strIp = null )
    {
        if( empty( $strIp ) || ( $strIp = trim( $strIp )) == '' )
        {
            return false;
        }

        $lIp = $this->ip2longCust( $strIp );

        $r = $this->_nredis->zRevRangeByScore( self::REDIS_SS_IPS, $lIp, -1, array( 'limit' => array( 0, 1 )));
        
        if( count( $r ) >= 1 )
        {
            $objIp = json_decode( $r[0] );
            if( $objIp->ip_begin <= $lIp && $lIp <= $objIp->ip_end )
            {
                return $objIp;
            }
            
            return false;
        }
        
        return false;
    }
    
    
    public function test()
    {
        return $this->getIpLocation( '59.38.112.95');
    }
    
    /**
     * @author( author='xbruce' )
     * @date( date = '2017年7月2日' )
     * @comment( comment = '解决因ip2long bug而引起的bug' )
     * @method( method = '' )
     * @op( op = '' )
     * @param string $strIp
     */
    private function ip2longCust( string $strIp )
    {
        if( preg_match( self::GREP_IP, $strIp ) && is_string( $strIp ) )
        {
            $arrIps = explode( '.', $strIp );
            $strIpTmp = '';
            foreach( $arrIps as $k => $v )
            {
                $strIpTmp .= intval( $v ) . '.';
            }
    
            $strIpTmp = rtrim( $strIpTmp, '\.' );
    
            return ip2long( $strIpTmp );
    
        }
    
        return false;
    }
    
}

// $redis = new Redis();

$redis = new Redis();
$redis->connect( '127.0.0.1', 6379 );
$redis->auth( 'fzq' );//redis requirepass
// $redis->select( 0 );

$iu = IpUtils::getInstance( $redis );
var_dump( $iu->initIpLibFromFile( '/Users/xbruce/git/phcms/czip.txt' ) );//初始化文件到临时文件
$iu->combineAdjacentIp();//合并同一地的相邻IP

$t = microtime( true );
for( $i = 0; $i < 100000; ++$i )
{
    $objRes = $iu->test();
}   

echo microtime( true ) - $t;
//在我的配置很低小本子上可以达到11094QPS以上 